Guard Salesforce::RoleSyncJob against missing parent records#850
Merged
Conversation
Defer the Contact_Editor_Affiliation__c write until both the parent Editor__c (school) and Contact (user) have been pushed to Salesforce, using the existing SalesforceRecordNotFound retry path. Co-authored-by: Cursor <cursoragent@cursor.com>
Test coverage91.34% line coverage reported by SimpleCov. |
`SalesforceSyncJob` retries on `SalesforceRecordNotFound`, so when `perform_now` raises it ActiveJob catches the error and re-enqueues the job rather than propagating. The new contexts asserted `raise_error`, which never matched. Mirror the existing `ContactSyncJob` spec pattern and assert `have_enqueued_job` (plus, for the side-effect tests, that no `Salesforce::Role` row is written). Co-authored-by: Cursor <cursoragent@cursor.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR makes Salesforce::RoleSyncJob resilient to Heroku Connect foreign-key external-ID races by deferring affiliation writes until the parent Editor__c (school) and Contact (user) mirror records exist locally with sfid populated, leveraging the existing SalesforceSyncJob retry mechanism.
Changes:
- Add a guard in
Salesforce::RoleSyncJobto raiseSalesforceRecordNotFoundunless both parent mirror records exist with non-nilsfid. - Update
Salesforce::RoleSyncJobspecs to provision parent mirror rows for the happy path. - Add new spec contexts to verify the job retries when parent mirror rows are missing or not yet synced (
sfid: nil).
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| app/jobs/salesforce/role_sync_job.rb | Adds parent-record readiness checks (via sfid) and triggers retry instead of writing failing affiliation rows. |
| spec/jobs/salesforce/role_sync_job_spec.rb | Extends coverage for retry scenarios and updates setup to satisfy the new parent guards. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Heroku Connect was leaving
Contact_Editor_Affiliation__crows permanentlyFAILEDin the mirror with errors like:This happens because
SchoolandRoleafter_commitcallbacks both fire in the same transaction and enqueue independent jobs (SchoolSyncJobandRoleSyncJob) on thesalesforce_syncqueue. The jobs use different concurrency keys, so the role write to the Heroku Connect mirror can be pushed to Salesforce before the Editor record lands. When that happens the lookup-by-external-id fails, Salesforce rejects the INSERT, and Heroku Connect does not auto-retry foreign-key resolution failures — the mirror row staysFAILEDforever.The same pattern happens against
Contactwhen a role is created before the upstream Pi Accounts → Salesforce Contact pipeline has materialised the user.Evidence from production
Investigating Editors in Salesforce with no
Contact_Editor_Affiliation__c:_hc_err:_hc_err = "Foreign key external ID … not found for field EditorUUID__c in entity Editor__c". The parentEditor__chad since synced (sfidpopulated,_hc_lastop = SYNCED), so the affiliation INSERT would now succeed — but Connect never retries.Contacterror and have noSalesforce::Contactmirror row at all (separate upstream issue, not in scope).00:01:04 UTCtoday for school6a228ff8-…, confirming the race is still actively breaking new affiliations.The 10 recoverable cases were re-synced manually; the SOQL count for not-rejected Editors with no CEA dropped 16 → 6 (3 missing-Contact + 3 by-design schools with only student / no roles).
Fix
Before writing to the affiliation mirror, verify the parent records exist in the local Heroku Connect mirror with
sfidpopulated (i.e. Salesforce has acknowledged them). If either parent isn't ready, raiseSalesforceRecordNotFound.SalesforceSyncJobalready declares:…so this slots into the existing retry mechanism (same one
ContactSyncJobalready relies on) without any new error class, queue, or backoff config.Behaviour
FAILEDpermanentlyFAILEDpermanentlyFAILEDrow in mirrorSALESFORCE_ENABLED=falseWhat this does not fix
Contactin Salesforce — needs investigation upstream of editor-api.FAILEDmirror rows already in production — those need a one-off cleanup (deletesfid: nilrows then re-enqueueRoleSyncJob); already done for the 10 recoverable cases above.Tests
spec/jobs/salesforce/role_sync_job_spec.rb:let!so the new guards pass.Salesforce::Schoolexists withsfid: nil→ raisesSalesforceRecordNotFound(Editor not yet synced)Salesforce::Schoolrow at all → raisesSalesforceRecordNotFoundSalesforce::Contactexists withsfid: nil→ raisesSalesforceRecordNotFound(Contact not yet synced)Salesforce::Contactrow at all → raisesSalesforceRecordNotFoundTest plan
Foreign key external ID … not founderrors oncontact_editor_affiliation__c— should drop to ~0 for new schoolsContact_Editor_Affiliation__crows appear in Salesforce within retry windowMade with Cursor